查看原文
其他

举杯邀Frida,对影成三题

一颗金柚子 看雪学院 2021-03-07

本文为看雪论坛优秀文章

看雪论坛作者ID:一颗金柚子



本文为看雪安卓高研2w班(6月班)优秀学员作品。


下面先让我们来看看讲师对学员学习成果的点评,以及学员的学习心得吧!


讲师点评

这位学员的解题思路有几个亮点:
1. Frida内置的JSON其实功能非常有限,但是使用谷歌的gson效果就完全不同了,几乎可以打印所有java对象,点赞!
2. “反编译的结果不能完全相信”,使用objection去内存中枚举类并打印调用栈,进一步确认类的名称和执行流程,这样是比较严谨的。
3. Frida hook nativelibc strcmp 判断第二个参数为“REJECT”,体现了尽量不要改so,而且修改系统库的思想,非常优秀。




学员感想


在7月建党节之际做了几道作业,回想起学习中国共产党18年革命斗争的历史经验时书本中指出这样一句话:“统一战线、武装斗争、党的建设,是中国共产党在中国革命中战胜敌人的三大法宝”。
其实,这句话到什么时刻都是具有一定的借鉴意义。那我们开门见山,接下来放的三道题的解题思路就是靠的三大法宝的“变形”。


ps. 题目附件请点击“阅读原文”下载。

现在,看雪《安卓高级研修班(网课)》9月班开始招生啦!点击查看详情报名吧~


1



0x01 第一题:

统一“Frida和Objection”的战线




要求是使用Frida,或者新建工程跑,但我个人是选择了Frida。   


如果成功,flag则会输出出来,看一下流程:

首先str是username 和 password的转字符串的拼接。


String str = MainActivity.this.username_et.getText().toString() + MainActivity.this.password_et.getText().toString();


经过VVVVV.VVVV判断为true之后就会告诉我们“恭喜您,成功了!flag is+str”

那关键的逻辑就在于这个VVVVV.VVVV里面,我们去看一下。


由于看到VVVV是静态函数,我们选择使用Java.use封装

function main(){ Java.perform(function() { Java.use("com.kanxue.pediy1.VVVVV").VVVV.implementation = function(x,y){ var result = this.VVVV(x,y); console.log("x,y,result:",x,y,result); return result; } })}setImmediate(main)

可以看到成功hook住了我们的输入:


接下来我们不要着急写Frida的代码(为什么?因为反编译的结果不能完全相信)。既然统一了战线,那先让我们的战友Objection去探探敌情,开远程比较方便:

./fri12820x64 -l 0.0.0.0:8888objection -N -h 192.168.1.103 -p 8888 -g com.kanxue.pediy1 exploreandroid hooking watch class_method com.kanxue.pediy1.VVVVV.VVVV --dump-args --dump-backtrace --dump-return


android hooking watch class_method com.kanxue.pediy1.VVVVV.eeeee --dump-args --dump-backtrace --dump-return

由于再看了一下前提条件,有个输入长度username+password必须长度为5,因此我们规定username:123 password:45。


可以看到VVVV调用了eeeee,此时验证了Jadx反编译的内容确实没错,我们可以放心大胆地使用Frida

然后输入是12345,返回值是object object。

由于返回的是object不便于观察,因此我们使用陈总的r0gson,并用frida打印:

function main(){ Java.perform(function() { Java.openClassFile("/data/local/tmp/r0gson.dex").load(); const gson = Java.use("com.r0ysue.gson.Gson"); Java.use("com.kanxue.pediy1.VVVVV").eeeee.implementation = function(x){ var result = this.eeeee(x); console.log("x,result:",x,gson.$new().toJson(result)); return result; } })}setImmediate(main)


噢明白了,那就是需要这个返回值要和”6f452303f18605510aac694b0f5736beebf110bf“的getBytes结果相等才行~   

我们去Android Stutio新建一个工程后尝试一下打印getBytes()的结果

String str = "6f452303f18605510aac694b0f5736beebf110bf";byte[] byt = str.getBytes();for (byte b : byt) { System.out.println(b);}

结果是:

[54,102,52,53,50,51,48,51,102,49,56,54,48,53,53,49,48,97,97,99,54,57,52,98,48,102,53,55,51,54,98,101,101,98,102,49,49,48,98,102]

那么我们就可以重新写如下脚本爆破“敌方大本营”

function firethehome(){ Java.perform(function(){ var VVVVV_Class = Java.use("com.kanxue.pediy1.VVVVV") console.log("VVVVV_Class:", VVVVV_Class) VVVVV_Class.eeeee.implementation=function(x){ var result = this.eeeee(x); console.log("VVVVV.eeeee is hook! x ,result",x,JSON.stringify(result)); return result; } var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log(ByteString); var pSign = Java.use("java.lang.String").$new("6f452303f18605510aac694b0f5736beebf110bf").getBytes(); console.log( ByteString.of(pSign).hex()); // 爆破5位 for(var i = 9999;i<100000;i++){ console.log("i="+i); var v = Java.use("java.lang.String").$new(String(i)); var vSign = VVVVV_Class.eeeee(v); console.log("vSign:",ByteString.of(vSign).hex()); if(ByteString.of(vSign).hex() == ByteString.of(pSign).hex()){ console.log("i="+i); break; } } })}setImmediate(firethehome)

可以发现打印了两次,那么这就是flag。




当然,如果不想要flag,也可以这么玩:

function main(){ Java.perform(function() { Java.openClassFile("/data/local/tmp/r0gson.dex").load(); const gson = Java.use("com.r0ysue.gson.Gson"); Java.use("com.kanxue.pediy1.VVVVV").eeeee.implementation = function(x){ var result = this.eeeee(x); console.log("x,result:",x,gson.$new().toJson(result)); var v = result; v = Java.array('byte',[54,102,52,53,50,51,48,51,102,49,56,54,48,53,53,49,48,97,97,99,54,57,52,98,48,102,53,55,51,54,98,101,101,98,102,49,49,48,98,102]) console.log(gson.$new().toJson(v)); return v; } })}setImmediate(main)

就是无论输入什么都会判断你是对的~


到了第二题,敌人发现了我们火力凶猛,想到使用动态加载Dex进行了初步的保护,别急,我们有第二法宝。


1

0x02 第二题:面对动态加载保护,

我们使用枚举ClassLoader来武装“Frida”斗争




题目要求是“没有要求,做出来即可”。

那我们回到Jadx,我们可以看MainActivity里面存在着动态加载Dex的操作:


str是username和password的字符串拼接,result是最后拿到flag的关键。


result刚开始赋值成false,经过try的处理,可以得知是调用了动态加载的classes.dex中的VVVVV类下的VVVV函数。

function main(){ Java.perform(function(){ Java.enumerateClassLoaders({ onMatch:function(loader){ try{ if(loader.findClass("com.kanxue.pediy1.VVVVV")){ console.log("success found com.kanxue.pediy1.VVVVV!",loader); Java.classFactory.loader = loader;//替换ClassLoader为DexClassLoader } }catch(e){ console.log("found error!",e); } },onComplete(){console.log("enum completed!")} }) Java.use("com.kanxue.pediy1.VVVVV").VVVV("12345"); });}function hookDex(){ Java.perform(function(){ Java.choose("com.kanxue.pediy1.MainActivity",{ onMatch:function(instance){ console.log("found instance:",instance); console.log("invoke loadDexClass!",instance.loadDexClass()); },onComplete(){} }) Java.choose("dalvik.system.DexClassLoader",{ onMatch:function(loader){ Java.classFactory.loader = loader; console.log("the Loader:", Java.classFactory.loader) },onComplete:function(){} }) var VVVVV_Class = Java.use("com.kanxue.pediy1.VVVVV") console.log("VVVVV_Class:", VVVVV_Class) VVVVV_Class.eeeee.implementation=function(x){ var result = this.eeeee(x); console.log("VVVVV.eeeee is hook!",result); return result; } })}

使用两步函数调用法:


frida -U  com.kanxue.pediy1 -l homework2.js

hookDex()

main()


发现已经成功拿到动态加载的DexClassLoader,并替换我们Frida的Java.ClassFactory,最后成功走到VVVVV类中hook住eeeee函数。



OK,敌方的小尾巴已经被我们抓住,开始爆破,主要修改hookDex()

function hookDex(){ Java.perform(function(){ Java.choose("com.kanxue.pediy1.MainActivity",{ onMatch:function(instance){ console.log("found instance:",instance); console.log("invoke loadDexClass!",instance.loadDexClass()); },onComplete(){} }) Java.choose("dalvik.system.DexClassLoader",{ onMatch:function(loader){ Java.classFactory.loader = loader; console.log("the Loader:", Java.classFactory.loader) },onComplete:function(){} }) var VVVVV_Class = Java.use("com.kanxue.pediy1.VVVVV") console.log("VVVVV_Class:", VVVVV_Class) var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log(ByteString); var pSign = Java.use("java.lang.String").$new("7c133979c8fc45943815792c0288300687cf0a16").getBytes(); console.log( ByteString.of(pSign).hex()); for(var i = 9999;i<100000;i++){ console.log("i="+i); var v = Java.use("java.lang.String").$new(String(i)); var vSign = VVVVV_Class.eeeee(v); console.log("vSign:",ByteString.of(vSign).hex()); if(ByteString.of(vSign).hex() == ByteString.of(pSign).hex()){ console.log("i="+i); break; } } })}



OK,回过头去验证一下:


竟然没有成功,那么,已经在java层分析没有大问题的前提下,我们要考虑去Native层看看有没有蹊跷

目标是libnative-lib.so中的StringFromJNI函数【因为动态加载dex的时候调用VVVV方法时用到了stringFromJNI的返回值做了参数】。

去IDA中查看:


一看,果然是这里,也就是最后传出来的参数也就是要当作VVVV参数的input【上图中的result】是+1后的结果。

因此我们逆向时需要减一,因此flag猜想是66999-1=66998


到了第三题,敌方已然是强弩之末,使出了最后的“杀手锏”保护措施——杀Frida进程。

莫慌,我们也有最后的第三法宝。


1

0x03 第三题:面对反Frida保护屡

试不爽的kill进程,采用Native Hook来强化建设我们的"Frida"





发现了检测frida的循环函数,跟进去查看:


因为strcmp此时若等于0(也就是第一个参数也为REJECT),会调用kill函数杀死进程。


有攻就有防,既然在so中出现了strcmp这么可爱的经典Native函数,关键它还是影响kill()的判断条件,我们就加以利用,四两拨千斤

那我们可以写如下脚本:

function hookstrcmp(){ Java.perform(function() { console.log("I am a Hook function"); var strcmp = Module.findExportByName("libc.so","strcmp");//这里发现无论“libnative-lib.so”还是“libc.so”都是一样的地址 console.log("find strcmp:",strcmp); Interceptor.attach(strcmp, { onEnter: function (args) { //hook住后打印strcmp的第一个参数和第二个参数的内容 if(ptr(args[1]).readCString().indexOf("REJECT")>=0){ console.log("[*] strcmp (" + ptr(args[0]).readCString() + "," + ptr(args[1]).readCString()+")"); this.isREJECT = true; }
},onLeave:function(retval){ if(this.isREJECT){ console.log("the REJECT's result :",retval); } } }); })}

而且这里设计的初衷是不影响其他正常的strcmp操作,因此我使用this来设置一个可以从传参到返回值都能用的一个标志(this.isREJECT)。


发现打印的都是第二个参数带REJECT的。

好了,接下来为了不让这一系列REJECT为第二参数的strcmp判断为0,我们需要replace这个函数的返回值,打算让它一直返回0x1。

function hookstrcmp(){ Java.perform(function() { console.log("I am a Hook function"); var strcmp = Module.findExportByName("libc.so","strcmp");//这里发现无论“libnative-lib.so”还是“libc.so”都是一样的地址 console.log("find strcmp:",strcmp); Interceptor.attach(strcmp, { onEnter: function (args) { //hook住后打印strcmp的第一个参数和第二个参数的内容 if(ptr(args[1]).readCString().indexOf("REJECT")>=0){ console.log("[*] strcmp (" + ptr(args[0]).readCString() + "," + ptr(args[1]).readCString()+")"); this.isREJECT = true; } },onLeave:function(retval){ if(this.isREJECT){ retval.replace(0x1); console.log("the REJECT's result :",retval); } } }); })}


这时候可以发现即使判断到接收信息是REJECT,我们的程序也不退出了~

Fire!!!

function hookDex(){ Java.perform(function(){ Java.choose("com.kanxue.pediy1.MainActivity",{ onMatch:function(instance){ console.log("found instance:",instance); console.log("invoke loadDexClass!",instance.loadDexClass()); },onComplete(){} }) Java.choose("dalvik.system.DexClassLoader",{ onMatch:function(loader){ Java.classFactory.loader = loader; console.log("the Loader:", Java.classFactory.loader) },onComplete:function(){} }) var VVVVV_Class = Java.use("com.kanxue.pediy1.VVVVV") console.log("VVVVV_Class:", VVVVV_Class) var ByteString = Java.use("com.android.okhttp.okio.ByteString"); console.log(ByteString); var pSign = Java.use("java.lang.String").$new("971b82e071392d8293e57b39fc5056c731517d4e").getBytes(); console.log( ByteString.of(pSign).hex()); //爆破 for(var i = 9999;i<100000;i++){ console.log("i="+i); var v = Java.use("java.lang.String").$new(String(i)); var vSign = VVVVV_Class.eeeee(v); console.log("vSign:",ByteString.of(vSign).hex()); if(ByteString.of(vSign).hex() == ByteString.of(pSign).hex()){ console.log("i="+i); break; } } })}function hookstrcmp(){ Java.perform(function() { console.log("I am a Hook function"); var strcmp = Module.findExportByName("libc.so","strcmp");//这里发现无论“libnative-lib.so”还是“libc.so”都是一样的地址 console.log("find strcmp:",strcmp); //Hook strcmp Interceptor.attach(strcmp, { onEnter: function (args) { //hook住后打印strcmp的第一个参数和第二个参数的内容 if(ptr(args[1]).readCString().indexOf("REJECT")>=0){ console.log("[*] strcmp (" + ptr(args[0]).readCString() + "," + ptr(args[1]).readCString()+")"); this.isREJECT = true; } },onLeave:function(retval){ if(this.isREJECT){ retval.replace(0x1); console.log("the REJECT's result :",retval); } } }); })}setImmediate(hookstrcmp);


可以发现在99999处i出现了两次,通过再次比较pSign发现两者一致,所以答案是99999。


最后测试的时候发现还是和第二题一样做了+1的操作,那我们还是99999-1=99998。

flag即为99998。


1



0x04 结语




论坛中还有很多大佬写了不同解法,建议一同食用,取长补短,共同进步~




- End -


看雪ID:一颗金柚子

https://bbs.pediy.com/user-879358.htm

  *本文由看雪论坛 一颗金柚子 原创,转载请注明来自看雪社区。




推荐文章++++

* Galgame汉化中的逆向(三):自定义字库分析

* Galgame汉化中的逆向(二):系统字库与文字编码

* 初试IDA&FRIDA联合调试简单ollvm保护的加密函数源码

* Frida加载和启动XServer

* 记一次so文件动态解密


好书推荐












公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



ps. 觉得对你有帮助的话,别忘点分享,点赞和在看,支持看雪哦~


“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存